﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using MyFramework.Command.Interfaces;

namespace MyFramework.Command.DelegateWatchCommand
{
    /// <summary>
    /// visitor algoritm
    /// </summary>
    internal static class Visitor
    {
        #region Fields

        /// <summary>
        /// The multi watch info
        /// </summary>
        private static MethodInfo multiWatchInfo;

        /// <summary>
        /// The collection watch info
        /// </summary>
        private static MethodInfo collectionWatchInfo;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes the <see cref="Visitor"/> class.
        /// </summary>
        static Visitor()
        {
            multiWatchInfo = typeof(PropertyWatchExtensions).GetMethod("MultiWatch");
            collectionWatchInfo = typeof(PropertyWatchExtensions).GetMethod("CollectionWatch");
        }

        #endregion

        /// <summary>
        /// Visits the specified expr.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="expr">The expr.</param>
        /// <returns></returns>
        public static PropertyWatchChain Visit<T>(Expression<Func<T, object>> expr)
            where T : INotifyPropertyChanged
        {
            var ret = Visit(expr.Body, null);
            if (ret == null)
                throw new Exception("Empty PropertyWatchChain. Lambda may not be a parameter.");
            return ret;
        }

        /// <summary>
        /// Visits the quote. Is like the public Visit() with type parameter, but not typed and must check the arg.
        /// </summary>
        /// <param name="expr">The expr.</param>
        /// <returns></returns>
        private static PropertyWatchChain VisitQuote(Expression expr)
        {
            if (expr.NodeType != ExpressionType.Quote)
                throw new ArgumentOutOfRangeException("Expression expr must be a constant lambda expression");
            var le = (LambdaExpression)((UnaryExpression)expr).Operand;
            if (le.Parameters.Count != 1)
                throw new ArgumentOutOfRangeException("Expression expr must have exactly one parameter");
            if (!typeof(INotifyPropertyChanged).IsAssignableFrom(le.Parameters[0].Type))
                throw new ArgumentOutOfRangeException("Lambda parameter must implement INotifyPropertyChanged");

            var ret = Visit(le.Body, null);
            if (ret == null)
                throw new Exception("Empty PropertyWatchChain. Lambda may not be a parameter.");
            return ret;
        }

        /// <summary>
        /// Visits the specified expr.
        /// </summary>
        /// <param name="expr">The expr.</param>
        /// <param name="chain">The chain.</param>
        /// <returns></returns>
        private static PropertyWatchChain Visit(Expression expr, PropertyWatchChain chain)
        {
            switch (expr.NodeType)
            {
                case ExpressionType.Convert:
                case ExpressionType.Quote:
                    return Visit(((UnaryExpression)expr).Operand, chain);
                case ExpressionType.Call:
                    return VisitMethodCall((MethodCallExpression)expr);
                case ExpressionType.MemberAccess:
                    return VisitMemberAccess((MemberExpression)expr, chain);
                case ExpressionType.Parameter:
                    return chain;
                default:
                    throw new Exception(string.Format("Unhandled expression type: '{0}'", expr.NodeType));
            }
        }

        /// <summary>
        /// Visits the method call.
        /// </summary>
        /// <param name="call">The call.</param>
        /// <returns></returns>
        private static PropertyWatchChain VisitMethodCall(MethodCallExpression call)
        {
            if (object.ReferenceEquals(call.Method.DeclaringType, multiWatchInfo.DeclaringType) && call.Method.Name == multiWatchInfo.Name)
                return VisitMultiWatch(call);

            if (object.ReferenceEquals(call.Method.DeclaringType, collectionWatchInfo.DeclaringType) && call.Method.Name == collectionWatchInfo.Name)
                return VisitCollectionWatch(call);

            throw new ArgumentOutOfRangeException(string.Format(
                    "Method '{0}' is called in expression. Only property access, convertion and methods '{1}' and '{2} are allowed.",
                    call.Method.Name, multiWatchInfo.Name));

        }

        /// <summary>
        /// Visits the collection watch.
        /// </summary>
        /// <param name="call">The call.</param>
        /// <returns></returns>
        private static PropertyWatchChain VisitCollectionWatch(MethodCallExpression call)
        {
            var arg0 = call.Arguments[0];
            var paramArray = ((NewArrayExpression)call.Arguments[1]).Expressions;

            PropertyWatchChain pattern;
            switch (paramArray.Count)
            {
                case 0:
                    pattern = null;
                    break;
                case 1:
                    pattern = VisitQuote(paramArray[0]);
                    break;
                default:
                    var chains = paramArray.Select(x => VisitQuote(x));
                    pattern = new PropertyWatchChain(new PropertyWatchMulti(chains));
                    break;
            }

            // The type parameter of CollectionWatch() is the type of the collection elements 
            //   and the type parameter of WatchCollection instance that should be created. 
            Type t = call.Method.GetGenericArguments()[0];
            Type tw = typeof(WatchCollection<>).MakeGenericType(t);
            var ctor = tw.GetConstructor(new Type[] { typeof(PropertyWatchChain) });
            // construct a WatchCollection instance:
            var cw = (IPropertyWatchTail)ctor.Invoke(new object[] { pattern });
            // make a chail with one element:
            var chain = new PropertyWatchChain(cw);

            return Visit(arg0, chain);
        }

        /// <summary>
        /// Visits the multi watch.
        /// </summary>
        /// <param name="call">The call.</param>
        /// <returns></returns>
        private static PropertyWatchChain VisitMultiWatch(MethodCallExpression call)
        {
            // handle MultiWatch(). Arg 0 is the path from the variable to the MultiWatch(), Type: MemberExpression/PropertyExpression 
            //   Arg 1 is a list of "continuations", Type: NewArrayInitExpression
            var arg0 = call.Arguments[0];
            var paramArray = ((NewArrayExpression)call.Arguments[1]).Expressions;
            var chains = paramArray.Select(x => VisitQuote(x));

            return Visit(arg0, new PropertyWatchChain(new PropertyWatchMulti(chains)));
        }

        /// <summary>
        /// Visits the member access.
        /// </summary>
        /// <param name="mex">The mex.</param>
        /// <param name="chain">The chain.</param>
        /// <returns></returns>
        private static PropertyWatchChain VisitMemberAccess(MemberExpression mex, PropertyWatchChain chain)
        {
            var mi = mex.Member;
            if (mi.MemberType != MemberTypes.Property)
                throw new ArgumentOutOfRangeException(string.Format(
                    "Expression accesses member '{0}.{1}' that is not a property ",
                    mi.DeclaringType.Name, mi.Name));

            if (chain == null)
            {
                // make the last/tail element
                chain = new PropertyWatchChain(new PropertyWatchTail(mi.Name));
            }
            else
            {
                // create and prepend an element:
                Type argType = mex.Expression.Type;
                if (!typeof(INotifyPropertyChanged).IsAssignableFrom(argType))
                    throw new ArgumentOutOfRangeException(
                        string.Format("All watched objects must implement INotifyPropertyChanged."
                        + " Property '{0}' applies to object of type '{1}' that doesnt. ",
                        mi.Name, argType.Name));
                var p = Expression.Parameter(typeof(INotifyPropertyChanged), "p");
                var cast = Expression.Convert(p, argType);
                var oneStepBody = Expression.MakeMemberAccess(cast, mex.Member);

                var pw = new PropertyWatch(Expression.Lambda<Func<INotifyPropertyChanged, INotifyPropertyChanged>>(oneStepBody, p));
                chain.Cons(pw);
            }

            return Visit(mex.Expression, chain);
        }
    }
}
